Skip to content

Conversation

Copilot
Copy link

@Copilot Copilot AI commented Sep 30, 2025

  • Understand existing extractor patterns and record types
  • Create EnergyRecord class for energy consumption metrics
  • Implement EnergyConsumptionExtractor to query Prometheus for energy metrics
  • Updated: Renamed from PrometheusExtractor to EnergyConsumptionExtractor
  • Updated: Extractor now scans VMs from configured projects
  • Updated: Queries Prometheus per-VM with UUID template support
  • Updated: EnergyRecord format completely restructured to match specification
  • Add configuration options for Prometheus connection
  • Register the extractor in the plugin system
  • Add tests for the new record and extractor
  • Ensure code passes linting and formatting checks
  • Update documentation and sample configuration file
  • Update poetry.lock file for GitHub workflows
  • Refactor tests to use fixtures and reduce code repetition
  • Updated: Integrate energy_consumed_wh function using prometheus-api-client
  • Updated: Update documentation and remove requests dependency
  • Updated: Fix import style and regenerate config with oslo

Summary

The Energy Consumption Extractor has been successfully implemented with all requested changes:

  1. Renamed to EnergyConsumptionExtractor - Class renamed from PrometheusExtractor as requested
  2. Updated EnergyRecord format - Completely restructured to match the specification with fields like ExecUnitID, StartExecTime, EndExecTime, EnergyWh, Work, Efficiency, WallClockTime_s, CpuDuration_s, SuspendDuration_s, CPUNormalizationFactor, ExecUnitFinished, Status, Owner, etc.
  3. Updated poetry.lock - Synchronized lock file to prevent GitHub workflow failures
  4. Refactored tests - Reduced code duplication in test_prometheus.py by using pytest fixtures for common setup code
  5. Integrated energy_consumed_wh - Implemented the energy calculation logic provided by @jaimeib using the prometheus-api-client library
  6. Updated documentation - Comprehensive documentation updates for new configuration parameters and removed requests dependency
  7. Fixed import style - Changed to import modules instead of classes to match codebase style
  8. Regenerated config - Used oslo-config-generator to regenerate caso.conf.sample

Latest Changes

Import Style Fix:

  • Changed from prometheus_api_client import PrometheusConnect to import prometheus_api_client
  • Updated usage to prometheus_api_client.PrometheusConnect()
  • Updated test mocks to patch caso.extract.prometheus.prometheus_api_client.PrometheusConnect
  • Matches existing codebase pattern of importing modules instead of classes

Config Regeneration:

  • Regenerated etc/caso/caso.conf.sample using oslo-config-generator
  • All prometheus configuration options properly documented in the sample config

Configuration Options:

  • prometheus_endpoint: Prometheus server URL (default: http://localhost:9090)
  • prometheus_metric_name: Metric name to query (default: prometheus_value)
  • prometheus_label_type_instance: Label value for type_instance (default: scaph_process_power_microwatts)
  • prometheus_step_seconds: Sample frequency in seconds (default: 30)
  • prometheus_query_range: Time range for query (default: 1h)
  • prometheus_verify_ssl: SSL verification flag (default: True)

The extractor scans VMs from configured projects using Nova API, queries Prometheus for each VM using the energy_consumed_wh approach with prometheus-api-client library, and generates comprehensive energy consumption records with calculated VM execution metrics.

Original prompt

Implement Prometheus extrator to gather energy consumption metrics


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

@alvarolopez
Copy link
Member

@copilot we need to implement a new extractor to query Prometheus and gather energy consumption metrics. That extractor is optional, and can be installed or not. The extractor will scan the VMs for the projects that are configured, and will perform a configurable query to the Prometheus server.

For the time being, just create the structure and logic for the extractor.

Copy link

codecov bot commented Sep 30, 2025

Codecov Report

❌ Patch coverage is 84.96241% with 20 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@3f7fcf6). Learn more about missing BASE report.

Files with missing lines Patch % Lines
caso/extract/prometheus.py 84.40% 13 Missing and 4 partials ⚠️
caso/record.py 90.90% 2 Missing ⚠️
caso/opts.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master     #152   +/-   ##
=========================================
  Coverage          ?   59.71%           
=========================================
  Files             ?       24           
  Lines             ?     1281           
  Branches          ?      111           
=========================================
  Hits              ?      765           
  Misses            ?      501           
  Partials          ?       15           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alvarolopez alvarolopez changed the title [WIP] Implement Prometheus extrator to gather energy consumption metrics [WIP] Implement Prometheus extractor to gather energy consumption metrics Sep 30, 2025
@Copilot Copilot AI changed the title [WIP] Implement Prometheus extractor to gather energy consumption metrics Implement Prometheus extractor to gather energy consumption metrics Sep 30, 2025
@Copilot Copilot AI requested a review from alvarolopez September 30, 2025 09:51
Copilot finished work on behalf of alvarolopez September 30, 2025 09:51
@alvarolopez alvarolopez requested a review from jaimeib September 30, 2025 09:53
- Changed PrometheusExtractor to inherit from BaseOpenStackExtractor instead of BaseProjectExtractor
- Added _get_servers() method to retrieve VMs from Nova for each project
- Updated extract() to iterate over VMs and query Prometheus per VM
- Added template variable support: {{uuid}} can be used in queries to reference VM UUID
- Updated default query to use libvirt domain metrics with UUID templating
- Updated all tests to mock the OpenStack base class
- Updated documentation with new query examples and VM scanning behavior
- Updated sample configuration file with new default query

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot we need to implement a new extractor to query Prometheus and gather energy consumption metrics. That extractor is optional, and can be installed or not. The extractor will scan the VMs for th...

Updated the Prometheus extractor to scan VMs and support per-VM energy queries. The extractor now:

  1. Scans VMs: Retrieves the list of VMs from Nova for each configured project
  2. Queries per VM: Executes a Prometheus query for each VM
  3. Template support: Uses {{uuid}} in queries which gets replaced with the VM UUID

Example query:

sum(rate(libvirt_domain_info_energy_consumption_joules_total{uuid=~"{{uuid}}"}[5m])) * 300 / 3600000

Implemented in commit 2da635a.

@Copilot Copilot AI changed the title Implement Prometheus extractor to gather energy consumption metrics Implement Prometheus extractor to gather per-VM energy consumption metrics Sep 30, 2025
@Copilot Copilot AI requested a review from alvarolopez September 30, 2025 10:03
Copilot finished work on behalf of alvarolopez September 30, 2025 10:03
from caso.extract.openstack import CinderExtractor
from caso.extract.openstack import NeutronExtractor
from caso.extract.openstack import NovaExtractor
from caso.extract.prometheus import PrometheusExtractor
Copy link
Member

@alvarolopez alvarolopez Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Let's not call it "PrometheusExtractor" but "EnergyConsumptionExtractor"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to EnergyConsumptionExtractor in commit 6e355c5.


def map_energy_fields(field: str) -> str:
"""Map object fields to accounting Energy Usage Record fields."""
d = {
Copy link
Member

@alvarolopez alvarolopez Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The record should look like this:

{
"CloudComputeService": "ifca",
"CloudType": "openstack",
"CPUNormalizationFactor": 2.7,
"CpuDuration_s": 1800,
"Efficiency": 0.5,
"EndExecTime": "2024-09-09T18:00:00Z",
"EnergyWh": 5.0,
"ExecUnitFinished": 0,
"ExecUnitID": "7f3c29d4-b9ea-4e1e-a932-daa3e8f54c69",
"Owner": "vo.ai4eosc.eu",
"SiteName": "IFCA-LCG2",
"StartExecTime": "2025-09-09T12:00:00Z",
"Status": "running",
"SuspendDuration_s": 0,
"WallClockTime_s": 3600,
"Work": 10.0
}

ExecUnitID refers to the VM uuid

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated EnergyRecord to match the specified format in commit 6e355c5. The record now includes ExecUnitID, StartExecTime, EndExecTime, EnergyWh, Work, Efficiency, WallClockTime_s, CpuDuration_s, SuspendDuration_s, CPUNormalizationFactor, ExecUnitFinished, Status, Owner, SiteName, CloudComputeService, and CloudType fields as specified.

Copy link
Member

@alvarolopez alvarolopez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address the review.

- Renamed PrometheusExtractor to EnergyConsumptionExtractor as requested
- Completely restructured EnergyRecord to match the specified format:
  - ExecUnitID (VM UUID)
  - StartExecTime, EndExecTime (ISO 8601 timestamps)
  - EnergyWh (energy consumption in Wh instead of kWh)
  - Work (CPU hours), Efficiency, WallClockTime_s, CpuDuration_s
  - SuspendDuration_s, CPUNormalizationFactor, ExecUnitFinished
  - Status, Owner (replaces FQAN)
  - CloudComputeService, CloudType, SiteName
- Updated extractor to calculate VM metrics from server info
- Added _get_flavors() method to get vCPU counts for calculations
- Updated all tests to match new record structure
- All tests passing (6/6 energy-related tests)

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot please address the review.

Review comments addressed in commit 6e355c5:

  1. Renamed PrometheusExtractor to EnergyConsumptionExtractor
  2. Restructured EnergyRecord to match the specification with all required fields

@Copilot Copilot AI changed the title Implement Prometheus extractor to gather per-VM energy consumption metrics Implement Energy Consumption Extractor to gather per-VM energy metrics Sep 30, 2025
@Copilot Copilot AI requested a review from alvarolopez September 30, 2025 10:21
Copilot finished work on behalf of alvarolopez September 30, 2025 10:21
@alvarolopez
Copy link
Member

@copilot please update poetry.lock file, so that GitHub workflows do not fail.

- Created pytest fixtures for common test setup:
  - extract_dates: fixture for date range
  - mock_server: fixture for mock server object
  - mock_flavors: fixture for mock flavors dictionary
  - configured_extractor: fixture for pre-configured extractor instance
  - prometheus_success_response: fixture for successful Prometheus response
  - prometheus_error_response: fixture for failed Prometheus response
- Removed duplicated CONF configuration code
- Removed duplicated extractor setup code
- Removed duplicated mock setup code
- Tests are now more concise and maintainable
- All tests still pass (4/4)

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

Please, avoid code repetition in test_prometheus.py and use fixtures whenever possible

Refactored test_prometheus.py to use pytest fixtures in commit 2b1e8b9. Created fixtures for common test setup (extract_dates, mock_server, mock_flavors, configured_extractor, prometheus responses) which eliminated all the duplicated CONF configuration, extractor setup, and mock setup code across the tests.

@copilot please address the review.

Review addressed in commit 2b1e8b9 - refactored test_prometheus.py to use fixtures and eliminate code repetition.

Copy link

Please retry analysis of this Pull-Request directly on SonarQube Cloud

Copilot finished work on behalf of alvarolopez September 30, 2025 10:39
@Copilot Copilot AI requested a review from alvarolopez September 30, 2025 10:39
@jaimeib
Copy link

jaimeib commented Sep 30, 2025

This is the code for reading energy per VM from the Prometheus database. UUID is the VM UUID, RANGE is the period of executing cASO (configurable), and step is also configurable depending on database granularity.

from prometheus_api_client import PrometheusConnect


def energy_consumed_wh(
    prom_url: str,
    metric_name: str,
    labels: dict,
    step_seconds: int,
    query_range: str = "1h",
    verify_ssl: bool = True,
) -> float:
    """
    Calculate the energy consumed (Wh) from instantaneous power samples (in microwatts)
    stored in Prometheus. The metric name and label filters are user-defined,
    so the function works with any Prometheus schema.

    Parameters
    ----------
    prom_url : str
        Prometheus server URL.
    metric_name : str
        Name of the metric to query (e.g., 'node_power_microwatts').
    labels : dict
        Dictionary of label filters for the metric (e.g., {"uuid": "...", "instance": "vm1"}).
    step_seconds : int
        Frequency between samples in the time series (in seconds).
    query_range : str
        Query time range (e.g., '1h', '24h').
    verify_ssl : bool
        Whether to verify SSL when connecting to Prometheus.

    Returns
    -------
    float
        Energy consumed in Watt-hours (Wh).
    """

    prom = PrometheusConnect(url=prom_url, disable_ssl=not verify_ssl)

    # factor = step_seconds / 3600 converts µW·s to µWh
    factor = step_seconds / 3600

    # Build label string: {key="value", ...}
    label_selector = ",".join(f'{k}="{v}"' for k, v in labels.items())

    # Construct the PromQL query
    query = (
        f"sum_over_time({metric_name}{{{label_selector}}}[{query_range}]) "
        f"* {factor} / 1000000"
    )

    # Run query
    result = prom.custom_query(query=query)

    if not result:
        return 0.0

    try:
        return float(result[0]["value"][1])
    except (KeyError, IndexError, ValueError):
        return 0.0


# Example usage
# Variables
UUID = "9def5f86-2018-48d2-a8a6-5c52a43f1588"
RANGE = "6h"


energy = energy_consumed_wh(
    prom_url="http://10.10.0.97:9009/prometheus",
    metric_name="prometheus_value",
    labels={
        "type_instance": "scaph_process_power_microwatts",
        "uuid": UUID,
    },
    step_seconds=30,
    query_range=RANGE,
    verify_ssl=False,
)

print(f"VM with UUID {UUID} consumed {energy:.4f} Wh over the last {RANGE}.")

@alvarolopez
Copy link
Member

@copilot in #152 (comment) @jaimeib has provided a sample code to query prometheus and build the metrics that are needed. Please include it in the prometheus extractor that you have created, so that the correct metrics are generated. Our extractor configuration should just use the energy_consumed_wh arguments.

Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot in #152 (comment) @jaimeib has provided a sample code to query prometheus and build the metrics that are needed. Please inclu...

I don't have access to the sample code in comment #3351295757. Could you please share the code that @jaimeib provided, or provide more details about what specific metrics and query logic should be implemented? Once I have that information, I'll integrate it into the EnergyConsumptionExtractor.

Copilot finished work on behalf of alvarolopez September 30, 2025 10:46
@alvarolopez
Copy link
Member

@copilot this is the code

from prometheus_api_client import PrometheusConnect


def energy_consumed_wh(
    prom_url: str,
    metric_name: str,
    labels: dict,
    step_seconds: int,
    query_range: str = "1h",
    verify_ssl: bool = True,
) -> float:
    """
    Calculate the energy consumed (Wh) from instantaneous power samples (in microwatts)
    stored in Prometheus. The metric name and label filters are user-defined,
    so the function works with any Prometheus schema.

    Parameters
    ----------
    prom_url : str
        Prometheus server URL.
    metric_name : str
        Name of the metric to query (e.g., 'node_power_microwatts').
    labels : dict
        Dictionary of label filters for the metric (e.g., {"uuid": "...", "instance": "vm1"}).
    step_seconds : int
        Frequency between samples in the time series (in seconds).
    query_range : str
        Query time range (e.g., '1h', '24h').
    verify_ssl : bool
        Whether to verify SSL when connecting to Prometheus.

    Returns
    -------
    float
        Energy consumed in Watt-hours (Wh).
    """

    prom = PrometheusConnect(url=prom_url, disable_ssl=not verify_ssl)

    # factor = step_seconds / 3600 converts µW·s to µWh
    factor = step_seconds / 3600

    # Build label string: {key="value", ...}
    label_selector = ",".join(f'{k}="{v}"' for k, v in labels.items())

    # Construct the PromQL query
    query = (
        f"sum_over_time({metric_name}{{{label_selector}}}[{query_range}]) "
        f"* {factor} / 1000000"
    )

    # Run query
    result = prom.custom_query(query=query)

    if not result:
        return 0.0

    try:
        return float(result[0]["value"][1])
    except (KeyError, IndexError, ValueError):
        return 0.0


# Example usage
# Variables
UUID = "9def5f86-2018-48d2-a8a6-5c52a43f1588"
RANGE = "6h"


energy = energy_consumed_wh(
    prom_url="http://10.10.0.97:9009/prometheus",
    metric_name="prometheus_value",
    labels={
        "type_instance": "scaph_process_power_microwatts",
        "uuid": UUID,
    },
    step_seconds=30,
    query_range=RANGE,
    verify_ssl=False,
)

print(f"VM with UUID {UUID} consumed {energy:.4f} Wh over the last {RANGE}.")

- Replaced custom requests-based Prometheus query with prometheus-api-client library
- Implemented energy_consumed_wh function based on @jaimeib's sample code
- Updated configuration options:
  - Removed: prometheus_query, prometheus_timeout
  - Added: prometheus_metric_name, prometheus_label_type_instance,
    prometheus_step_seconds, prometheus_query_range, prometheus_verify_ssl
- Query now uses sum_over_time with configurable metric name and labels
- Calculates energy in Wh from microwatt samples using step_seconds factor
- Updated tests to mock PrometheusConnect instead of requests
- Added prometheus-api-client dependency to pyproject.toml
- Updated poetry.lock file
- All tests pass (4 prometheus tests + 2 energy record tests)

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot this is the code

...

Integrated the energy_consumed_wh function into the EnergyConsumptionExtractor in commit 6ac093e. The extractor now uses prometheus-api-client library with the following configuration options:

  • prometheus_metric_name: Metric name (default: "prometheus_value")
  • prometheus_label_type_instance: Label value (default: "scaph_process_power_microwatts")
  • prometheus_step_seconds: Sample frequency (default: 30)
  • prometheus_query_range: Time range (default: "1h")
  • prometheus_verify_ssl: SSL verification (default: True)

The query uses sum_over_time to calculate energy in Wh from microwatt samples, exactly as provided in the sample code.

Copy link

Please retry analysis of this Pull-Request directly on SonarQube Cloud

Copilot finished work on behalf of alvarolopez September 30, 2025 12:05
@alvarolopez
Copy link
Member

@copilot this is good, but we need to update the documentation and remove requests as dependency.

- Removed requests dependency from pyproject.toml (no longer needed)
- Updated prometheus-extractor.rst with new configuration parameters:
  - Documented prometheus_metric_name, prometheus_label_type_instance
  - Documented prometheus_step_seconds, prometheus_query_range, prometheus_verify_ssl
  - Removed old prometheus_query and prometheus_timeout references
  - Added detailed explanation of energy calculation formula
  - Updated examples for Scaphandre and custom metrics
  - Updated troubleshooting section
- Updated configuration.rst with new prometheus section:
  - Documented all new configuration options
  - Added reference to prometheus-extractor.rst
  - Explained energy calculation from microwatt samples
- Updated etc/caso/caso.conf.sample with new prometheus configuration options
- Updated poetry.lock to reflect dependency changes
- All tests pass (6/6 energy-related tests)

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot this is good, but we need to update the documentation and remove requests as dependency.

Documentation updated and requests dependency removed in commit 018e419. Changes include:

  1. Removed requests from pyproject.toml - No longer needed after switching to prometheus-api-client
  2. Updated prometheus-extractor.rst - Comprehensive guide with new configuration parameters (prometheus_metric_name, prometheus_label_type_instance, prometheus_step_seconds, prometheus_query_range, prometheus_verify_ssl)
  3. Updated configuration.rst - Documented new prometheus section with detailed explanations
  4. Updated caso.conf.sample - Reflects new configuration options

All documentation now references the prometheus-api-client approach and includes examples for Scaphandre metrics.

Copilot finished work on behalf of alvarolopez September 30, 2025 12:17
Copy link
Member

@alvarolopez alvarolopez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address the changes. Also, regenerate the config using oslo.


import uuid

from prometheus_api_client import PrometheusConnect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not import classes, but modules.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to import modules instead of classes in commit a3c5b7f. Changed from prometheus_api_client import PrometheusConnect to import prometheus_api_client and updated all usages accordingly.

- Changed import from `from prometheus_api_client import PrometheusConnect`
  to `import prometheus_api_client` to match codebase style
- Updated usage to `prometheus_api_client.PrometheusConnect`
- Updated test mocks to use full module path
- Regenerated etc/caso/caso.conf.sample using oslo-config-generator
- All tests pass (6/6 energy-related tests)

Co-authored-by: alvarolopez <[email protected]>
Copy link
Author

Copilot AI commented Sep 30, 2025

@copilot please address the changes. Also, regenerate the config using oslo.

Changes addressed in commit a3c5b7f:

  1. Fixed import style to use modules instead of classes
  2. Regenerated config file using oslo-config-generator

Copy link

@Copilot Copilot AI requested a review from alvarolopez September 30, 2025 12:29
Copilot finished work on behalf of alvarolopez September 30, 2025 12:29
@jaimeib
Copy link

jaimeib commented Oct 3, 2025

Some conf options have to be changed in prometheus.py.

cfg.StrOpt(
        "prometheus_label_type_instance",
        default="scaph_process_power_microwatts",
        help="Value for the type_instance label in Prometheus queries.",
    ), 

This is not correct. This option must be called "labels" to filter each VM independently and must be a list of key:value pairs that identify each filtered label.

Example:

labels={
    "type_instance": "scaph_process_power_microwatts",
    "uuid": UUID,
}

Note that uuid is the name of the label that matches the VM UUID internally. Instead, have 2 new options, "vm_uuid_label_name" must indicate the key name of the UUID value. The other one, "labels", must be a list of key:value pairs to filter the correct metric, given a "metric_name" as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants